/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { Logger, ObjectUtils, PluginFileUtil, PluginStore } from '../../framework';
import {
  FilePathConstants,
  ObfuscationConstants,
  PluginConstants,
  PrefixConstants,
  TemplateConstants,
} from '../constants';
import { HMRouterExtensionContext } from '../HMRouterExtensionContext';
import { HMServiceResult, RouterInfo } from '../model';

export class ObfuscationProcessor {
  private context: HMRouterExtensionContext;

  constructor(context: HMRouterExtensionContext) {
    this.context = context;
  }

  execute(): void {
    Logger.debug(this.context.node.getNodeName(), `Start to obfuscation...`);

    const buildProfileOpt: any = this.context.moduleContext?.getBuildProfileOpt();

    // 1. Check if obfuscation is enabled
    if (!this.isEnableObfuscation(buildProfileOpt)) {
      Logger.info(
        this.context.node.getNodeName(),
        'This compilation does not turn on code obfuscation, skip hmrouter_obfuscation_rules.txt file generation'
      );
      return;
    }

    // 2. Generate the obfuscation rule file
    const obfuscationString = this.generateObfuscationRuleFile();

    // 3. Handle the special logic of HAR package
    this.handleHarPackageLogic(obfuscationString);

    // 4. update buildProfileOpt
    this.context.moduleContext?.setBuildProfileOpt(buildProfileOpt);
  }

  /**
   * Generate the obfuscation rule file
   * @returns The generated obfuscation rule string
   */
  private generateObfuscationRuleFile(): string {
    const obfuscationFilePath = PluginFileUtil.pathResolve(
      this.context.config.modulePath,
      FilePathConstants.OBFUSCATION_FILE_NAME
    );

    const routerMap = JSON.parse(
      PluginFileUtil.readFileSync(this.context.config.getRouterMapDir()).toString()
    ).routerMap;

    const obfuscationString = this.buildObfuscatedStrings(routerMap);

    PluginFileUtil.ensureFileSync(obfuscationFilePath);
    PluginFileUtil.writeFileSync(obfuscationFilePath, obfuscationString);

    Logger.info(this.context.node.getNodeName(), `Generate obfuscation rule file successfully, filePath: ${obfuscationFilePath}`);

    return obfuscationString;
  }

  /**
   * Handle the special logic of HAR package
   * @param obfuscationString The obfuscation rule string
   */
  private handleHarPackageLogic(obfuscationString: string): void {
    if (this.context.moduleContext.getModuleType() === PluginConstants.HAR_MODULE_NAME) {
      const consumerRulesPath = PluginFileUtil.pathResolve(
        this.context.config.modulePath,
        FilePathConstants.CONSUMER_FILE_NAME
      );

      PluginFileUtil.ensureFileSync(consumerRulesPath);
      PluginFileUtil.writeFileSync(consumerRulesPath, obfuscationString);
    }
  }

  // Check if the current buildModel has obfuscation configuration enabled
  private isEnableObfuscation(buildProfileOpt: any): boolean {
    const currentBuildMode = PluginStore.getInstance().get<string>('buildMode');
    const buildOption = buildProfileOpt.buildOptionSet?.find((item: any) => item.name === currentBuildMode);

    if (!buildOption) {
      return false;
    }

    // Ensure the necessary objects exist and get ruleOptions
    const arkOptions = ObjectUtils.ensureNestedObject(buildOption, ['arkOptions']);
    const obfuscationOptions = ObjectUtils.ensureNestedObject(arkOptions, ['obfuscation']);
    const ruleOptions = ObjectUtils.ensureNestedObject(obfuscationOptions, ['ruleOptions']);

    const isObfuscationEnabled = !!ruleOptions.enable;

    // If obfuscation isn't enabled in ruleOptions, return directly
    if (!isObfuscationEnabled) {
      return false;
    }

    // If auto-add obfuscation rules is enabled
    if (this.context.config?.autoObfuscation) {
      this.addHMRouterObfuscationRules(ruleOptions, obfuscationOptions);
    }

    return true;
  }

  /**
   * Helper function to add HMRouter obfuscation rule files to the build options.
   * @param ruleOptions The ruleOptions object from build options.
   * @param obfuscationOptions The obfuscation object from build options.
   */
  private addHMRouterObfuscationRules(ruleOptions: any, obfuscationOptions: any): void {
    const obfuscationFilePath = FilePathConstants.CURRENT_DELIMITER + FilePathConstants.OBFUSCATION_FILE_NAME;
    ruleOptions.files = this.appendFilePathSafely(ruleOptions.files, obfuscationFilePath);

    // Add consumer rules for HAR modules
    if (this.context.moduleContext.getModuleType() === PluginConstants.HAR_MODULE_NAME) {
      const consumerRulesPath = FilePathConstants.CURRENT_DELIMITER + FilePathConstants.CONSUMER_FILE_NAME;
      obfuscationOptions.consumerFiles = this.appendFilePathSafely(obfuscationOptions.consumerFiles, consumerRulesPath);
    }
  }

  /**
   * Helper function to safely append a path to a target which might be undefined, a string, or an array.
   * Avoids adding duplicate paths to arrays.
   * @param target The current value (string, string[], or undefined).
   * @param filePath The path to append.
   * @returns The updated value (string[]).
   */
  private appendFilePathSafely(target: string | string[] | undefined, filePath: string): string[] {
    if (typeof target === 'string') {
      // If the current target is a string and different from the new path, create an array
      return target === filePath ? [target] : [target, filePath];
    } else if (Array.isArray(target)) {
      // If it's an array, add the path only if it doesn't exist
      if (!target.includes(filePath)) {
        target.push(filePath);
      }
      return target;
    } else {
      // If target is undefined, null, or anything else, start a new array
      return [filePath];
    }
  }

  private buildObfuscatedStrings(routerMap: RouterInfo[]) {
    let srcPathArr = [
      ...new Set(
        routerMap.map((routerInfo) => {
          return FilePathConstants.CURRENT_DELIMITER + routerInfo.pageSourceFile!;
        })
      ),
    ];
    let classNameArr = [
      ...new Set(
        routerMap
          .filter((routerInfo) => {
            return routerInfo.name.includes('__');
          })
          .map((routerInfo) => {
            return routerInfo.customData.name!;
          })
      ),
    ];
    let functionName = [
      ...new Set(
        routerMap
          .filter((routerInfo) => {
            return routerInfo.name.includes(PrefixConstants.SERVICE_PREFIX);
          })
          .map((routerInfo) => {
            return (routerInfo.customData as HMServiceResult).functionName!;
          })
      ),
    ];
    let watchFunctionName = [
      ...new Set(
        routerMap
          .filter((routerInfo) => {
            return routerInfo.buildFunction;
          })
          .map((routerInfo) => {
            return routerInfo.buildFunction + TemplateConstants.WARP_BUILDER;
          })
      ),
    ];
    return (
      ObfuscationConstants.KEEP_FILE_NAME +
      FilePathConstants.LINE_BREAK +
      srcPathArr
        .concat(ObfuscationConstants.KEEP_PROPERTY_NAME, functionName)
        .concat(ObfuscationConstants.KEEP_GLOBAL_NAME, classNameArr, watchFunctionName)
        .join(FilePathConstants.LINE_BREAK)
    );
  }
}
